Skip to content

Add TypeScript definition generation and --types-output CLI option#23

Open
pkaminski wants to merge 1 commit intomasterfrom
codex/add-optional-typescript-definitions-generation
Open

Add TypeScript definition generation and --types-output CLI option#23
pkaminski wants to merge 1 commit intomasterfrom
codex/add-optional-typescript-definitions-generation

Conversation

@pkaminski
Copy link
Member

@pkaminski pkaminski commented Mar 4, 2026

Motivation

  • Provide optional generated TypeScript .d.ts output to give NodeJS/Firebase consumers a typed view of the Fireplan schema.
  • Document the new TypeScript generation feature and its limitations in the README.md.

Description

  • Add a new type_generator.js module implementing TypeGenerator that converts the YAML schema and simple function signatures into a FirebaseData TypeScript type.
  • Wire up generation in the compiler by exporting generateTypes and extending transformFile to accept a typesOutput path and write the .d.ts file when provided.
  • Add a new CLI option -t, --types-output in the fireplan executable to specify the TypeScript output file.
  • Update README.md with usage instructions and a summary of what the generated types cover and when they fall back to unknown.

Testing

  • No automated tests were added or run for this change.

Codex Task


This change is Reviewable

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the Fireplan compiler by adding support for generating TypeScript definition files directly from the YAML schema. This feature aims to improve developer experience for NodeJS/Firebase consumers by providing type safety and better autocompletion for their data structures, making it easier to interact with Firebase data defined by Fireplan rules. The changes involve adding a new module for type conversion, extending the CLI with a new output option, and updating documentation to reflect these capabilities.

Highlights

  • TypeScript Definition Generation: Introduced the ability to generate TypeScript .d.ts definition files from Fireplan YAML schemas, providing typed views for NodeJS/Firebase consumers.
  • New CLI Option: Added a new command-line interface option, --types-output (or -t), to specify the output path for the generated TypeScript definition file.
  • Core Logic for Type Generation: Implemented a new type_generator.js module containing the TypeGenerator class, responsible for parsing the YAML schema and converting it into a FirebaseData TypeScript type.
  • Compiler Integration: Integrated the TypeScript generation into the main compiler, allowing transformFile to accept the typesOutput path and write the .d.ts file when provided.
  • Documentation Update: Updated the README.md to include usage instructions for the new --types-output option and detailed what aspects of the Fireplan schema are covered by the generated TypeScript types.
Changelog
  • README.md
    • Added a new section demonstrating the --types-output CLI option.
    • Included a detailed section explaining TypeScript Definition Generation, outlining covered features and limitations.
  • fireplan
    • Added the --types-output option to yargs for specifying the TypeScript definition file path.
    • Modified the compiler.transformFile call to pass the new typesOutput argument.
  • index.js
    • Imported the generateTypes function from the new type_generator.js module.
    • Exported generateTypes for external use.
    • Updated transformFile to accept a typesOutput parameter.
    • Cloned the source YAML for type generation to avoid modifying the original source during rule compilation.
    • Added logic to create the output directory and write the generated TypeScript definition file if typesOutput is provided.
  • type_generator.js
    • Added a new module containing the TypeGenerator class.
    • Implemented methods for loading function definitions, resolving function types, inferring expression types, and generating the final TypeScript string.
    • Included logic to handle primitive types, required keywords, oneOf(...) constraints, wildcard keys, and .more: true properties.
    • Exported the generateTypes function to initiate the type generation process.
Activity
  • The author, pkaminski, explicitly stated that no automated tests were added or run for this change.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a valuable feature for generating TypeScript definitions from the Fireplan schema, complete with a new CLI option and documentation. However, a critical security vulnerability was identified in the TypeGenerator class where property names are not correctly escaped during code generation, potentially allowing for code injection. Additionally, there are a couple of high-severity issues: a potential bug in index.js involving a shallow clone that could lead to the type generator operating on a mutated schema, and in type_generator.js, the type inference logic is overly aggressive and may produce incorrect types for complex expressions. Addressing these points will significantly improve the robustness and security of this new feature.

index.js Outdated
if (!output) output = input.replace(/\.ya?ml$/, '') + '.json';
const rawSource = fs.readFileSync(input, 'utf8');
const source = jsyaml.load(rawSource, {filename: input, schema: jsyaml.DEFAULT_SAFE_SCHEMA});
const sourceForTypes = typesOutput ? clone(source, false) : null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The clone(source, false) performs a shallow clone. However, the transform function mutates the source object deeply (e.g., by modifying source.root and source.functions). This can lead to incorrect TypeScript type generation because generateTypes will receive a partially modified object. A deep clone is required here to ensure the type generator works on a pristine copy of the source.

Suggested change
const sourceForTypes = typesOutput ? clone(source, false) : null;
const sourceForTypes = typesOutput ? clone(source) : null;

Comment on lines +50 to +60
inferExpressionType(expression) {
if (!_.isString(expression)) return 'unknown';
const parsed = this.parseConstraint(expression);
const oneOfType = this.oneOfType(parsed.expression);
if (oneOfType) return oneOfType;
if (/\bstring\b/.test(parsed.expression)) return 'string';
if (/\bnumber\b/.test(parsed.expression)) return 'number';
if (/\bboolean\b/.test(parsed.expression)) return 'boolean';
if (/\bany\b/.test(parsed.expression)) return 'unknown';
return this.resolveFunctionReferenceType(parsed.expression);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of inferExpressionType uses broad regular expressions (/\bstring\b/.test(...)) to detect types. This can lead to incorrect type inference. For example, an expression like next.foo === "a string value" would be incorrectly inferred as type string instead of unknown (since it's a complex boolean expression).

The logic should be more conservative. It should only infer a primitive type if the expression is just that type's name (e.g., string). The resolveFunctionReferenceType method already handles this case correctly for single-word expressions. The regex-based checks for string, number, boolean, and any are redundant and error-prone, and should be removed. The function should rely on oneOfType and resolveFunctionReferenceType for simple cases and fall back to unknown for anything more complex, as documented in the README.

  inferExpressionType(expression) {
    if (!_.isString(expression)) return 'unknown';
    const parsed = this.parseConstraint(expression);
    const expr = parsed.expression;

    const oneOfType = this.oneOfType(expr);
    if (oneOfType) return oneOfType;

    const funcType = this.resolveFunctionReferenceType(expr);
    if (funcType) return funcType;

    return 'unknown';
  }

}

propertyName(key) {
return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The propertyName function fails to properly escape single quotes and backslashes when wrapping a property name in single quotes. An attacker who can control the keys in the input YAML file (e.g., through a malicious pull request to a repository using fireplan) can inject arbitrary TypeScript code into the generated output file. If this generated file is subsequently imported into a project or processed by tools that execute code from TypeScript files, it could lead to arbitrary code execution. Using JSON.stringify is a safer way to handle property names that are not valid identifiers, as it correctly escapes special characters and wraps the string in double quotes.

Suggested change
return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`;
return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : JSON.stringify(key);

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f721b7e271

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const value = yaml[key];
const constraint = _.isString(value) ? value : value && value['.value'];
return {
key: key.charAt(0) === '$' ? '$' : this.propertyName(key),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip encrypted key annotations before emitting TS properties

The generated type keys currently use raw YAML key names for non-wildcard children, so Fireplan annotations like /encrypted or /encrypted[...] are emitted as part of the property name instead of being removed. In schemas that rely on encrypted path annotations (which transformBranch strips before building runtime rules), the .d.ts no longer matches actual Firebase paths and will type-check against incorrect field names.

Useful? React with 👍 / 👎.

}

propertyName(key) {
return key.match(/^[$a-zA-Z_][a-zA-Z0-9_$]*$/) ? key : `'${key}'`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape quoted property names in generated type literals

propertyName wraps non-identifier keys in single quotes without escaping, so a valid Firebase key containing an apostrophe (for example owner'sName) produces invalid TypeScript syntax in the generated .d.ts. This causes downstream TS compilation/parsing failures for schemas with such keys.

Useful? React with 👍 / 👎.

@pkaminski pkaminski force-pushed the codex/add-optional-typescript-definitions-generation branch 2 times, most recently from 620be1b to 7710d02 Compare March 10, 2026 23:51
Copy link
Member Author

@pkaminski pkaminski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkaminski resolved 5 discussions.
Reviewable status: 0 of 5 files reviewed, all discussions resolved.

@pkaminski pkaminski force-pushed the codex/add-optional-typescript-definitions-generation branch 2 times, most recently from 24cab12 to d588b8a Compare March 11, 2026 06:40
@pkaminski pkaminski requested a review from snoack March 11, 2026 06:41
@pkaminski
Copy link
Member Author

+r:@snoack

@pkaminski pkaminski force-pushed the codex/add-optional-typescript-definitions-generation branch from d588b8a to 019e804 Compare March 11, 2026 06:51
@pkaminski
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 019e8046b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rows.push(`${_.repeat(' ', indent + 2)}${key}${required ? '' : '?'}: ${type}`);
}
});
if (node.moreAllowed) rows.push(`${_.repeat(' ', indent + 2)}[key: string]: any`);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid emitting duplicate string index signatures

When an object contains both a wildcard child and .more: true, this always appends a second string index signature ([key: string]: any) even though typeString already emitted one for the wildcard entry, so the generated .d.ts contains two [...: string] signatures and fails TypeScript compilation (TS2374). This breaks the documented wildcard+.more scenario instead of widening the wildcard type.

Useful? React with 👍 / 👎.

const rows = ['{'];
_.forEach(node.entries, entry => {
const type = this.typeString(entry.node, indent + 2);
const required = entry.required || entry.wildcard || type === 'any';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep any-typed properties optional unless required

This forces every property inferred as any to be required in the generated type, even when the schema does not use the required keyword. In Fireplan rules, child presence is only enforced via required, so schemas like foo: any are valid when foo is absent, but the emitted type incorrectly requires foo and causes false type errors for consumers.

Useful? React with 👍 / 👎.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All required controls is whether the emitted property has a ? suffix, and AFAIK there's no point in having an any-typed property be optional.

@pkaminski pkaminski force-pushed the codex/add-optional-typescript-definitions-generation branch from 019e804 to 60f7c44 Compare March 11, 2026 07:07
Copy link
Member Author

@pkaminski pkaminski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkaminski made 1 comment and resolved 2 discussions.
Reviewable status: 0 of 7 files reviewed, all discussions resolved (waiting on snoack).

const rows = ['{'];
_.forEach(node.entries, entry => {
const type = this.typeString(entry.node, indent + 2);
const required = entry.required || entry.wildcard || type === 'any';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All required controls is whether the emitted property has a ? suffix, and AFAIK there's no point in having an any-typed property be optional.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant